import os
import atexit
import threading
import psutil, signal
from typing import Optional
from playwright.sync_api import sync_playwright
PLAYWRIGHT_HEADLESS = 1
# 全局常量，放在文件顶部
DETAIL_URL = "https://www.douyin.com/aweme/v1/web/aweme/detail/"


def build_params(aweme_id: str) -> dict:
    # 只放“相对固定”的字段，动态签名由页面 JS 注入
    return {
        "device_platform": "webapp",
        "aid": "6383",
        "channel": "channel_pc_web",
        "aweme_id": aweme_id,
        "version_code": "190500",
        "browser_name": "Edge",
        "os_name": "Windows",
    }
class DouyinBrowser:
    """懒加载 + 全局单例 + 自动自愈 + 常驻首页 Page"""
    _instance: Optional["DouyinBrowser"] = None
    _glock = threading.Lock()

    def __init__(self):
        self._lock = threading.Lock()
        self._pw = None
        self._browser = None
        self._context = None
        self._page = None   # ⭐ 常驻 page

        hv = os.environ.get("PLAYWRIGHT_HEADLESS", "1").lower()
        self._headless = hv in ("", "1", "true", "yes", "on")
        self._proxy = os.environ.get("PLAYWRIGHT_PROXY", "").strip()
        atexit.register(self.close)

    @classmethod
    def instance(cls) -> "DouyinBrowser":
        if cls._instance is not None:
            return cls._instance
        with cls._glock:
            if cls._instance is None:
                cls._instance = cls()
        return cls._instance

    def _ensure_started(self):
        with self._lock:
            # 1. 如果已有 page，先探活
            if self._page and not self._page.is_closed():
                try:
                    # 🔹 轻量探活：取页面标题，不触发重加载
                    self._page.title()
                    return
                except Exception:
                    # 探活失败，说明挂了
                    self._safe_close()

            # 2. 到这里说明没 page 或挂了 → 重启浏览器
            print("[INIT] 启动 Playwright 浏览器...")
            if not self._pw:
                self._pw = sync_playwright().start()

            launch_kwargs = {"headless": self._headless}
            self._browser = self._pw.chromium.launch(**launch_kwargs)
            self._context = self._browser.new_context()
            self._context.set_default_timeout(30000)  # Linux 可以调大点

            # ⭐ 打开首页并保持
            self._page = self._context.new_page()
            self._page.goto("https://www.douyin.com/", wait_until="domcontentloaded", timeout=30000)

    @property
    def browser(self):
        self._ensure_started()
        return self._browser

    @property
    def context(self):
        self._ensure_started()
        return self._context

    def get_page(self):
        """返回常驻首页 page"""
        self._ensure_started()
        return self._page

    def new_page(self):
        """需要额外 page 时再开新的"""
        self._ensure_started()
        return self._context.new_page()

    def _safe_close(self):
        """更彻底的关闭，避免残留进程"""
        try:
            if self._page and not self._page.is_closed():
                self._page.close()
        except Exception:
            pass
        try:
            if self._context:
                self._context.close()
        except Exception:
            pass
        try:
            if self._browser:
                self._browser.close()
        except Exception:
            pass

        # 🔹 强杀残留 headless_shell 进程
        for proc in psutil.process_iter(attrs=['pid', 'name']):
            try:
                if proc.info['name'] and "headless_shell" in proc.info['name']:
                    proc.send_signal(signal.SIGKILL)
            except Exception:
                pass

        self._page = None
        self._context = None
        self._browser = None

    def close(self):
        try:
            self._safe_close()
        finally:
            try:
                if self._pw:
                    self._pw.stop()
            except Exception:
                pass
            self._pw = None

        # 在全局page.py 里的 DouyinBrowser 类里加上：


    def fetch_detail(self, aweme_id: str, retries: int = 3, warmup: bool = True) -> dict:
        """
        在“常驻首页 page”里发起 detail 请求，返回 JSON。
        - 自动在视频页预热（让 cookie/签名就绪）
        - JS 端不盲目 parse，返回 ctype/text 便于排错
        """
        page = self.get_page()

        # # 1) 先去对应视频页“点火”，通常更稳定
        # if warmup:
        #     try:
        #         page.goto(f"https://www.douyin.com/video/{aweme_id}",
        #                   wait_until="domcontentloaded", timeout=15000)
        #         # 轻等 300ms，给前端 SDK 注入/签名预留时间
        #         page.wait_for_timeout(300)
        #     except Exception:
        #         # 有时候首屏没打 detail，滚动一下再说
        #         try:
        #             page.mouse.wheel(0, 1200)
        #             page.wait_for_timeout(200)
        #         except Exception:
        #             pass

        # 2) 更稳健的 JS（不直接 JSON.parse；总是保留原始 text 片段）
        js = """
        async (input) => {
          try {
            const qs = new URLSearchParams(input.params).toString();
            const url = input.url + "?" + qs;
            const r = await fetch(url, {
              credentials: "include",
              headers: {
                "Accept": "application/json, text/plain, */*",
                "Referer": "https://www.douyin.com/"
              }
            });
            const ctype = (r.headers.get("content-type") || "").toLowerCase();
            const txt = await r.text();
            let data = null, jsonError = null;
            if (ctype.includes("application/json")) {
              try { data = JSON.parse(txt); } catch (e) { jsonError = String(e); }
            }
            return {
              ok: r.ok, status: r.status, url: r.url,
              ctype, text: txt ? txt.slice(0, 5000) : "",
              hasData: !!data, data, jsonError
            };
          } catch (e) {
            return { ok: false, error: String(e) };
          }
        }
        """

        params = {"url": DETAIL_URL, "params": build_params(aweme_id)}

        last = None
        for i in range(retries):
            res = page.evaluate(js, params)
            last = res
            # 成功拿到 JSON
            if res.get("hasData") and isinstance(res.get("data"), dict):
                data = res["data"]
                # 抖音惯例：需要有 aweme_detail 才算真正成功
                if data.get("aweme_detail") or data.get("status_code") == 0:
                    return data
            # 否则小等/滚动再试一次
            try:
                page.mouse.wheel(0, 800)
            except Exception:
                pass
            page.wait_for_timeout(300)

        # 3) 仍失败，给出尽可能多的上下文
        raise RuntimeError(
            "❌ fetch_detail 失败：\n"
            f"  aweme_id: {aweme_id}\n"
            f"  ok={last.get('ok')} status={last.get('status')} ctype={last.get('ctype')}\n"
            f"  jsonError={last.get('jsonError')} error={last.get('error')}\n"
            f"  url={last.get('url')}\n"
            f"  text_prefix={repr((last.get('text') or '')[:200])}"
        )


# 全局单例
douyin_browser = DouyinBrowser.instance()